package com.google.code.swingonfire.component.clock;

import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.text.DateFormat;
import java.util.Calendar;

import javax.swing.JComponent;

import com.google.code.swingonfire.GraphicsUtils;
import com.google.code.swingonfire.animation.Animation;
import com.google.code.swingonfire.animation.Animator;
import com.google.code.swingonfire.image.ComponentBasedImageProvider;
import com.google.code.swingonfire.utility.Utils;

public class JClock extends JComponent implements JClockModelListener, Animation, MouseListener, MouseMotionListener
{

    private static final long serialVersionUID = 5461837071216897908L;

    private final ComponentBasedImageProvider backgroundImageProvider = new ComponentBasedImageProvider(this, true)
    {

        /**
         * {@inheritDoc}
         */
        @Override
        protected void drawImage(Graphics2D g, int width, int height)
        {
            GraphicsUtils.clear(g, width, height);

            int size = Math.min(width, height);

            g.translate(width / 2, height / 2);

            renderer.drawBackground(JClock.this, g, size);
        }

    };

    private final ComponentBasedImageProvider foregroundImageProvider = new ComponentBasedImageProvider(this, true)
    {

        /**
         * {@inheritDoc}
         */
        @Override
        protected void drawImage(Graphics2D g, int width, int height)
        {
            GraphicsUtils.clear(g, width, height);

            g.translate(width / 2, height / 2);

            renderer.drawForeground(JClock.this, g, width, height);
        }

    };

    private final JClockState state = new JClockState();
    private final JClockModel model;
    private final JClockRenderer renderer = new DefaultJClockRenderer();

    private boolean animated = true;
    private boolean editable = true;

    private JClockHandType draggingType = null;
    private Calendar draggingCalendar = null;

    public JClock()
    {
        this(new DefaultJClockModel());
    }

    public JClock(JClockModel model)
    {
        super();

        this.model = model;

        setPreferredSize(new Dimension(64, 64));
        model.addClockModelListener(this);

        addMouseListener(this);
        addMouseMotionListener(this);

        update(model.getSelected(), false);
    }

    public JClockState getState()
    {
        return state;
    }

    public JClockModel getModel()
    {
        return model;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void clockModelSelectionChanged(Calendar oldValue, Calendar newValue, boolean correctRotation)
    {
        Calendar selected = model.getSelected();
        int rotation = 0;
        
        if (correctRotation) {
            int compare = compare(oldValue, newValue);
    
            if (compare < 0)
            {
                rotation = 1;
            }
            else if (compare > 0)
            {
                rotation = -1;
            }
        }
System.out.println("dings");
        update(selected, rotation, isAnimated(), null);
    }

    public boolean isAnimated()
    {
        return animated;
    }

    public void setAnimated(boolean animated)
    {
        this.animated = animated;
    }

    public boolean isEditable()
    {
        return editable;
    }

    public void setEditable(boolean editable)
    {
        this.editable = editable;
    }

    protected boolean update(Calendar calendar, boolean animate)
    {
        return update(calendar, 0, animate, null);
    }

    protected boolean update(Calendar calendar, int rotation, boolean animate, JClockHandType adjusting)
    {
        boolean updated = false;

        updated |=
            update(JClockHandType.YEAR, toYear(calendar), rotation, animated && animate
                && ((adjusting != null) ? adjusting.isAnimateYearOnAdjusting() : true));
        updated |=
            update(JClockHandType.MONTH, toMonth(calendar), rotation, animated && animate
                && ((adjusting != null) ? adjusting.isAnimateMonthOnAdjusting() : true));
        updated |=
            update(JClockHandType.DAY, toDay(calendar), rotation, animated && animate
                && ((adjusting != null) ? adjusting.isAnimateDayOnAdjusting() : true));
        updated |=
            update(JClockHandType.SUN, toSun(calendar), rotation, animated && animate
                && ((adjusting != null) ? adjusting.isAnimateSunOnAdjusting() : true));
        updated |=
            update(JClockHandType.AMPM, toAMPM(calendar), rotation, animated && animate
                && ((adjusting != null) ? adjusting.isAnimateAMPMOnAdjusting() : true));
        updated |=
            update(JClockHandType.HOUR, toHour(calendar), rotation, animated && animate
                && ((adjusting != null) ? adjusting.isAnimateHourOnAdjusting() : true));
        updated |=
            update(JClockHandType.MINUTE, toMinute(calendar), rotation, animated && animate
                && ((adjusting != null) ? adjusting.isAnimateMinuteOnAdjusting() : true));
        updated |=
            update(JClockHandType.SECOND, toSecond(calendar), rotation, animated && animate
                && ((adjusting != null) ? adjusting.isAnimateSecondOnAdjusting() : true));

        return updated;
    }

    protected boolean update(JClockHandType type, double value, int rotation, boolean animate)
    {
        boolean startAnimation = animated && animate;
        boolean updated = state.set(type, value, rotation, startAnimation);

        if ((startAnimation) && (updated))
        {
            Animator.start(this);
        }

        if (updated)
        {
            foregroundImageProvider.updateNeeded();
            repaint();
        }

        return updated;
    }

    private double toYear(Calendar calendar)
    {
        return calendar.get(Calendar.YEAR);
    }

    private double toMonth(Calendar calendar)
    {
        return calendar.get(Calendar.MONTH);
    }

    private double toDay(Calendar calendar)
    {
        return calendar.get(Calendar.DAY_OF_MONTH);
    }

    private double toSun(Calendar calendar)
    {
        return toHour(calendar) / 24d;
    }

    private double toAMPM(Calendar calendar)
    {
        return (calendar.get(Calendar.AM_PM) == Calendar.AM) ? 0.5 : 1.5;
    }

    private double toHour(Calendar calendar)
    {
        return calendar.get(Calendar.HOUR_OF_DAY) + (toMinute(calendar) / 60d);
    }

    private double toMinute(Calendar calendar)
    {
        return calendar.get(Calendar.MINUTE);
    }

    private double toSecond(Calendar calendar)
    {
        return calendar.get(Calendar.SECOND);
    }

    private int compare(Calendar left, Calendar right)
    {
        if (left == right)
        {
            return 0;
        }

        if (left == null)
        {
            return -1;
        }
        else if (right == null)
        {
            return 1;
        }

        double leftValue = toYear(left);
        double rightValue = toYear(right);

        if (leftValue < rightValue)
        {
            return -1;
        }
        else if (leftValue > rightValue)
        {
            return 1;
        }

        leftValue = toMonth(left);
        rightValue = toMonth(right);

        if (leftValue < rightValue)
        {
            return -1;
        }
        else if (leftValue > rightValue)
        {
            return 1;
        }

        leftValue = toDay(left);
        rightValue = toDay(right);

        if (leftValue < rightValue)
        {
            return -1;
        }
        else if (leftValue > rightValue)
        {
            return 1;
        }

        leftValue = toSun(left);
        rightValue = toSun(right);

        if (leftValue < rightValue)
        {
            return -1;
        }
        else if (leftValue > rightValue)
        {
            return 1;
        }

        leftValue = toAMPM(left);
        rightValue = toAMPM(right);

        if (leftValue < rightValue)
        {
            return -1;
        }
        else if (leftValue > rightValue)
        {
            return 1;
        }

        leftValue = toHour(left);
        rightValue = toHour(right);

        if (leftValue < rightValue)
        {
            return -1;
        }
        else if (leftValue > rightValue)
        {
            return 1;
        }

        leftValue = toMinute(left);
        rightValue = toMinute(right);

        if (leftValue < rightValue)
        {
            return -1;
        }
        else if (leftValue > rightValue)
        {
            return 1;
        }

        leftValue = toSecond(left);
        rightValue = toSecond(right);

        if (leftValue < rightValue)
        {
            return -1;
        }
        else if (leftValue > rightValue)
        {
            return 1;
        }

        return 0;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public double animationStep(double delay, double time, double fraction)
    {
        boolean updated = getState().update(delay);

        foregroundImageProvider.updateNeeded();
        repaint();

        return (updated) ? 0 : Double.NaN;
    }

    @Override
    protected void paintComponent(Graphics g)
    {
        g.drawImage(backgroundImageProvider.getImage(), 0, 0, this);
        g.drawImage(foregroundImageProvider.getImage(), 0, 0, this);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void mouseClicked(MouseEvent e)
    {
        // intentionally left blank
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void mousePressed(MouseEvent e)
    {
        if ((isEditable()) && (draggingType == null))
        {
            Point point = e.getPoint();
            int width = getWidth();
            int height = getHeight();
            int x = point.x - (width / 2);
            int y = point.y - (height / 2);
            JClockHandType type = renderer.getHit(this, x, y, width, height);

            if (type != null)
            {
                draggingType = type;
                draggingCalendar = (Calendar) getModel().getSelected().clone();

                if (state.setSelected(draggingType))
                {
                    foregroundImageProvider.updateNeeded();
                    repaint();
                }

                e.consume();
            }

        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void mouseReleased(MouseEvent e)
    {
        if (draggingType != null)
        {
            JClockHandState handState = state.get(draggingType);
            Double override = handState.getOverride();
            handState.setOverride(null);

            if (override != null)
            {
                handState.setCurrent(override.doubleValue());
            }
            
            model.setSelected(draggingCalendar, false);

            draggingType = null;
            draggingCalendar = null;

            e.consume();
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void mouseEntered(MouseEvent e)
    {
        // intentionally left blank
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void mouseExited(MouseEvent e)
    {
        // intentionally left blank
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void mouseDragged(MouseEvent e)
    {
        if (draggingType != null)
        {
            Point point = e.getPoint();
            int width = getWidth();
            int height = getHeight();
            int x = point.x - (width / 2);
            int y = point.y - (height / 2);
            double d = Math.sqrt((x * x) + (y * y));
            double theta = (d != 0) ? Math.acos(-y / d) : 0;

            if (x < 0)
            {
                theta = (Math.PI * 2) - theta;
            }

            JClockHandState handState = state.get(draggingType);
            double current = handState.getCurrent();
            double currentTheta = renderer.toAngle(this, draggingType, current);

//            System.out.println(String.format("Theta=%.3f, Current=%.3f, CurrentTheta=%.3f, difffTheta=%.3f",
//                (theta / (Math.PI * 2)) * 360, current, (currentTheta / (Math.PI * 2)) * 360,
//                ((theta - currentTheta) / (Math.PI * 2)) * 360));

            double diff =
                Utils.clamp(0, renderer.fromAngle(this, draggingType, theta - currentTheta), handState.getMaximum());

            if (diff > (handState.getMaximum() / 2))
            {
                diff = -handState.getMaximum() + diff;
            }

            double value = Utils.clamp(0, current + diff, handState.getMaximum());

            handState.setOverride(value);

            switch (draggingType)
            {
                case YEAR:
                    throw new UnsupportedOperationException("Not yet supported");
                case MONTH:
                    throw new UnsupportedOperationException("Not yet supported");
                case DAY:
                    throw new UnsupportedOperationException("Not yet supported");
                case SUN:
                    throw new UnsupportedOperationException("Not yet supported");
                case AMPM:
                    throw new UnsupportedOperationException("Not yet supported");
                case HOUR:
                {
                    int hour = (int) Math.round(value);
                    int currentHour = draggingCalendar.get(Calendar.HOUR);
                    int adv = hour - currentHour;

                    if (diff < 0)
                    {
                        adv = Utils.clamp(0, adv, -12);
                    }
                    else
                    {
                        adv = Utils.clamp(0, adv, 12);
                    }

                    draggingCalendar.add(Calendar.HOUR, adv);
                    break;
                }
                case MINUTE:
                {
                    int minute = (int) Math.round(value);
                    int currentMinute = draggingCalendar.get(Calendar.MINUTE);
                    int adv = minute - currentMinute;

                    if (diff < 0)
                    {
                        adv = Utils.clamp(0, adv, -60);
                    }
                    else
                    {
                        adv = Utils.clamp(0, adv, 60);
                    }

                    draggingCalendar.add(Calendar.MINUTE, adv);
                    break;
                }
                case SECOND:
                    draggingCalendar.add(Calendar.MILLISECOND, (int) (diff * 1000));
                    break;
                default:
                    throw new UnsupportedOperationException("Not yet supported");
            }

//            System.out.println(DateFormat.getDateTimeInstance().format(draggingCalendar.getTime()) + ", " + diff);

            update(draggingCalendar, (diff >= 0) ? 1 : -1, false, draggingType);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void mouseMoved(MouseEvent e)
    {
        if ((isEditable()) && (draggingType == null))
        {
            Point point = e.getPoint();
            int width = getWidth();
            int height = getHeight();
            int x = point.x - (width / 2);
            int y = point.y - (height / 2);

            if (state.setSelected(renderer.getHit(this, x, y, width, height)))
            {
                foregroundImageProvider.updateNeeded();
                repaint();
            }

            e.consume();
        }
    }

}
